package com.kyist.frp.controller;

import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.kyist.frp.bean.FrpConnEnum;
import com.kyist.frp.bean.FrpcInfo;
import com.kyist.frp.config.FrpConfig;
import com.kyist.frp.utils.FileUtils;
import com.kyist.frp.utils.IpUtils;
import com.kyist.frp.utils.ObjSerializeUtils;
import org.apache.log4j.Logger;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.web.bind.annotation.*;

import javax.annotation.PreDestroy;
import java.io.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 接收数据
 *
 * @Author kyist
 * @Date 2021/11/23 22:30
 */
@RestController
@RequestMapping
@EnableScheduling
public class FrpController implements Serializable {

    public static final String SERIALIZEFILE = System.getProperty("user.dir")+System.getProperty("file.separator") +  "frpcMap.json";



    private static final Logger log = Logger.getLogger(FrpController.class);
    public static FrpConfig frpConfig;

    public static boolean isStart = false;
    private static int max_remote_addr = 20;
    //记录连接的客户端，已run_id 为key
    public static Map<String,FrpcInfo> frpcMap =  new ConcurrentHashMap<>();
    //初始化配置文件
    static {
        //存在序列化文件
        try {
            frpConfig = ObjSerializeUtils.strToObj(FileUtils.readAimFile(FrpConfig.SERIALIZEFILE),FrpConfig.class);
            //获取临时的map
            Map<String,JSONObject> tempMap = ObjSerializeUtils.strToObj(FileUtils.readAimFile(FrpController.SERIALIZEFILE),HashMap.class);
            //遍历填充到map中
            tempMap.forEach((key, jsonObject) -> {
                frpcMap.put(key, jsonObject.toBean(FrpcInfo.class));
            });
            System.out.println(frpcMap);

        } catch (Exception e) {
            System.out.println("没有序列化文件");
            frpConfig = new FrpConfig();
            frpConfig.init();
        }finally {
            try {
                String max = FrpConfig.prop.getProperty("max_remote_addr");
                if (max != null){
                    max_remote_addr = Integer.parseInt(max);
                }
            } catch (NumberFormatException e) {
                e.printStackTrace();
            }
        }
    }





    /**
     * 获取配置文件
     * @return frpConfig
     */
    @GetMapping("getConf")
    @ResponseBody
    public Object getConf(){
//        if ("allow".equals(frpConfig.getMode())){ //获取白名单
//            return frpConfig.getAllow();
//        }else if ("block".equals(frpConfig.getMode()) ){
//            return frpConfig.getBlock();
//        }
        return frpConfig;
    }


    @GetMapping("getFrpcMap")
    public  Map<String,FrpcInfo> getFrpcMap(){
        return  frpcMap;
    }




    /**
     * frps 发出的请求
     * @param data data
     * @return
     */
    @PostMapping("handler")
    public ResponseEntity<Map<String, Object>> handler(@RequestBody Map<String, Object>  data){
        Object op = data.get("op");
        if (op != null) {
            return new ResponseEntity<>(handlerData(data), HttpStatus.OK);
        }else{
            return new ResponseEntity<>(null, HttpStatus.BAD_REQUEST);
        }
    }

    /**
     * 添加Ip
     * @param ip 外来ip
     * @return res
     */
    @GetMapping("addIp/{ip}")
    public ResponseEntity<String> addIp(@PathVariable String ip){
        System.out.println(ip);
        String mode = frpConfig.getMode();
        String msg = "success";
        if ("no".equalsIgnoreCase(mode)){
            msg = "no 模式 ，无需添加ip";
        }else if ("allow".equalsIgnoreCase(mode)){
            //判断IP是否在allow中,在就返回否
            List<String> allow = frpConfig.getAllow();
            if (!allow.contains(ip.trim())) {
                frpConfig.setAllow(ip.trim());
            }
        }else if ("block".equalsIgnoreCase(mode)){
            //判断IP是否在block中,在就返回否
            List<String> block = frpConfig.getBlock();
            if (!block.contains(ip.trim())) {
                frpConfig.setBlock(ip.trim());
            }
        }
        exit(); //序列化配置文件
        return  new ResponseEntity<>(msg,HttpStatus.OK);
    }

    /**
     * 设置模式
     * @param mode no，allow block
     * @return 结果
     */
    @GetMapping("setMode/{mode}")
    public String setMode(@PathVariable String mode){
        frpConfig.setMode(mode);
        exit(); //序列化配置文件
        return "success";
    }

    /**
     * 删除名单ip
     */
    @GetMapping("delConfIp/{modeValue}/{ip}")
    public String delConfIp(@PathVariable String modeValue, @PathVariable String ip){
        if ("allow".equals(modeValue)){
            frpConfig.getAllow().remove(ip);
        }
        if ("block".equals(modeValue)){
            frpConfig.getBlock().remove(ip);
        }
        exit(); //序列化配置文件
        return "success";
    }


    /**
     * 处理 frps发来的数据信息
     * @param data data
     * @return map
     */
    private Map<String,Object> handlerData(Map<String, Object> data) {
        Map<String, Object> map = new HashMap<>();
        boolean reject = false;
        //unchange 为false 是客户端连接会失败
        boolean unchange = true;
        try {
            JSONObject jsonObject = JSONUtil.parseObj(data);
            String op = jsonObject.getStr("op");
            JSONObject content = jsonObject.getJSONObject("content");
            switch (op) {
                case "Ping":
                    //添加客户端信息，判断是否允许该客户端上线
                    reject = addFrpcInfo(content);
                    break;
                case "Login":
                    isStart = false;  //重置状态
                    break;
                case "NewProxy":
                    addFrpcProxyInfo(content);
                    break;
                case "NewUserConn":
                    String remote_addr = content.getStr("remote_addr");
                    String ip = remote_addr.split(":")[0];
                    reject = checkIp(ip);
                    if (!reject) {  //ip检查通过，将Ip信息添加到frpcMap
                        String proxy_name = content.getStr("proxy_name").split(":")[0];
                        String run_id = content.getJSONObject("user").getStr("run_id");
                        addIpToInfo(remote_addr,proxy_name,run_id);
                    }

                    break;
                case "NewWorkConn":
                    break;
                default:
            }
            if("Ping".equals(op)){
                log.info(FrpConnEnum.valueOf(op).getValue() + data );
            }else if (reject){  //拒绝连接
                log.warn(FrpConnEnum.valueOf(op).getValue() + data );
            }else{
                log.warn(FrpConnEnum.valueOf(op).getValue() + data );
            }
        } catch (Exception e) {
            e.printStackTrace();
            reject = true;
        }
        map.put("reject",reject);
        if (reject) {
            map.put("reject_reason","invalid user");
        }else{
            map.put("unchange", unchange);
        }
        return map;
    }

    /**
     * 将Ip信息添加到frpcInfo
     * @param ip ip
     * @param proxy_name name
     * @param runid id
     */
    private void addIpToInfo(String ip, String proxy_name, String runid) {
        ip = ip.split(":")[0];
        FrpcInfo frpcInfo = frpcMap.get(runid);
        if (frpcInfo != null){
            JSONObject proxyContent = frpcInfo.getProxyInfo().get(proxy_name);
            if (proxyContent != null){
                JSONArray rids = proxyContent.getJSONArray("remote_addr");
                if (rids == null){
                    rids = new JSONArray();
                }
                if (!rids.contains(ip)) {
                    rids.put(ip);
                    rids.set("");
                }
                //将展示的数量保持到max_remote_addr
                for (;rids.size()> max_remote_addr;) {
                    System.out.println(rids.remove(0));
                }

                proxyContent.putOpt("remote_addr",rids);
//                String remote_addr = proxyContent.getStr("remote_addr","");
//                //记录最新的一次连接
//                proxyContent.putOpt("remote_addr",remote_addr);
            }
        }

    }

    /**
     * 往frpcMap中添加代理
     * @param content JSONObject
     */
    private void addFrpcProxyInfo(JSONObject content) throws InterruptedException {
        //因为不用user作为key，所以自旋,等待ping 将frpc将 数据填充，
        while (!isStart){
            Thread.sleep(200);
        }
        JSONObject user = content.getJSONObject("user");
        String run_id = user.getStr("run_id");
        FrpcInfo frpcInfo = frpcMap.get(run_id);
        String proxy_name = content.getStr("proxy_name","");
        //直接存代理信息
        if (frpcInfo != null) {
            frpcInfo.setProxyInfo(proxy_name, content);
        }
    }

    //ping的时候，添加frpc客户端信息到frpcMap
    private boolean addFrpcInfo(JSONObject content) {

        String run_id = content.getJSONObject("user").getStr("run_id");

        FrpcInfo frpcInfo = frpcMap.getOrDefault(run_id,new FrpcInfo());
        //判断状态是否允许连接
        String status = frpcInfo.getStatus();
        if ("on".equalsIgnoreCase(status)) {
            JSONObject user = content.getJSONObject("user");
            JSONObject metas = content.getJSONObject("metas");

            //属性填充
            frpcInfo.setRun_id(run_id);
            frpcInfo.setUser(user.getStr("user",""));
            frpcInfo.setMetas(user.getJSONObject("metas"));
            //更新ping的时间
            frpcInfo.setLastPing(new Date());
            //存放到map中
            frpcMap.put(run_id, frpcInfo);

            if (!isStart) {
                isStart = true;
            }
            return false;
        }
        //拒绝连接
        return true;

    }

    /**
     * 连接ip黑白名单判断
     * @param ip 外连Ip
     * @return bool
     */
    private boolean checkIp(String ip) {
        String mode = frpConfig.getMode();
        if ("allow".equalsIgnoreCase(mode)){
            //判断IP是否在allow中,在就返回否
           // return !frpConfig.getAllow().contains(ip);
            return !IpUtils.checkIpInList(ip,frpConfig.getAllow());
        }else if ("block".equalsIgnoreCase(mode)){
            //判断IP是否在block中,在就返回否
            return IpUtils.checkIpInList(ip,frpConfig.getBlock());
        }
        return false;
    }

    /**客户端下线*/
    @ResponseBody
    @GetMapping("/changeStatus/{run_id}/{status}")
    public ResponseEntity<String> changeStatus(@PathVariable String run_id,@PathVariable String status){
        FrpcInfo frpcInfo = frpcMap.get(run_id);
        String msg = "客户端状态修改成功";
        if (frpcInfo != null){
            frpcInfo.setStatus(status);
            msg = "客户端状态修改成功";
        }
        return new ResponseEntity<>(msg, HttpStatus.OK);
    }




    //创建定时任务，定时清理已过期的frpc信息,1分钟一次
    @Scheduled(cron = "0 */1 * * * ?")
    public void removeOverdueFrpc(){

        try {
            List<String> ids = new ArrayList<>();
            if (!frpcMap.isEmpty()) {
                frpcMap.forEach((run_id, frpcInfo) -> {
                    Date lastPing = frpcInfo.getLastPing();
                    String status = frpcInfo.getStatus();
                    //记录超过60秒不发出ping的frpc run_id
                    if ( //下线的保留3小时，上线的1分钟保留
                         ("on".equalsIgnoreCase(status) && System.currentTimeMillis() - lastPing.getTime() > 60000)
                        || ("off".equalsIgnoreCase(status) && System.currentTimeMillis() - lastPing.getTime() > 180000)
                    ) {
                        ids.add(run_id);
                    }
                });
            }
            if (!ids.isEmpty()){
                System.out.println(ids);
                ids.forEach(run_id->frpcMap.remove(run_id));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    //程序退出时执行
    @PreDestroy
    public void exit(){

        try {
            String strConf = ObjSerializeUtils.ObjToJsonStr(frpConfig);
            FileUtils.saveFile(FrpConfig.SERIALIZEFILE,strConf);

            String strFrpcMap = ObjSerializeUtils.ObjToJsonStr(frpcMap);
            FileUtils.saveFile(FrpController.SERIALIZEFILE,strFrpcMap);
            log.error(frpConfig.toString());
        } catch (Exception e) {
            e.printStackTrace();
        }


    }


}
